/*
 * Copyright [2013-2021], Alibaba Group Holding Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.polardbx.common.utils.binlog;

import com.alibaba.polardbx.common.type.MySQLStandardFieldType;

import java.nio.charset.Charset;

/**
 * 处理下MySQL json二进制转化为可读的字符串
 *
 * @author agapple 2016年6月30日 上午11:26:17
 * @since 1.0.22
 */
public class JsonConversion {

    private static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

    // JSON TYPE
    public static final int JSONB_TYPE_SMALL_OBJECT = 0x0;
    public static final int JSONB_TYPE_LARGE_OBJECT = 0x1;
    public static final int JSONB_TYPE_SMALL_ARRAY = 0x2;
    public static final int JSONB_TYPE_LARGE_ARRAY = 0x3;
    public static final int JSONB_TYPE_LITERAL = 0x4;
    public static final int JSONB_TYPE_INT16 = 0x5;
    public static final int JSONB_TYPE_UINT16 = 0x6;
    public static final int JSONB_TYPE_INT32 = 0x7;
    public static final int JSONB_TYPE_UINT32 = 0x8;
    public static final int JSONB_TYPE_INT64 = 0x9;
    public static final int JSONB_TYPE_UINT64 = 0xA;
    public static final int JSONB_TYPE_DOUBLE = 0xB;
    public static final int JSONB_TYPE_STRING = 0xC;
    public static final int JSONB_TYPE_OPAQUE = 0xF;
    public static final char JSONB_NULL_LITERAL = '\0';
    public static final char JSONB_TRUE_LITERAL = '\1';
    public static final char JSONB_FALSE_LITERAL = '\2';

    /*
     * The size of offset or size fields in the small and the large storage
     * format for JSON objects and JSON arrays.
     */
    public static final int SMALL_OFFSET_SIZE = 2;
    public static final int LARGE_OFFSET_SIZE = 4;

    /*
     * The size of key entries for objects when using the small storage format
     * or the large storage format. In the small format it is 4 bytes (2 bytes
     * for key length and 2 bytes for key offset). In the large format it is 6
     * (2 bytes for length, 4 bytes for offset).
     */
    public static final int KEY_ENTRY_SIZE_SMALL = (2 + SMALL_OFFSET_SIZE);
    public static final int KEY_ENTRY_SIZE_LARGE = (2 + LARGE_OFFSET_SIZE);

    /*
     * The size of value entries for objects or arrays. When using the small
     * storage format, the entry size is 3 (1 byte for type, 2 bytes for
     * offset). When using the large storage format, it is 5 (1 byte for type, 4
     * bytes for offset).
     */
    public static final int VALUE_ENTRY_SIZE_SMALL = (1 + SMALL_OFFSET_SIZE);
    public static final int VALUE_ENTRY_SIZE_LARGE = (1 + LARGE_OFFSET_SIZE);

    public static Json_Value parse_value(int type, LogBuffer buffer, long len, String charsetName) {
        return parse_value(type, buffer, len, Charset.forName(charsetName));
    }

    public static Json_Value parse_value(int type, LogBuffer buffer, long len, Charset charset) {
        buffer = buffer.duplicate(buffer.position(), (int) len);
        switch (type) {
        case JSONB_TYPE_SMALL_OBJECT:
            return parse_array_or_object(Json_enum_type.OBJECT, buffer, len, false, charset);
        case JSONB_TYPE_LARGE_OBJECT:
            return parse_array_or_object(Json_enum_type.OBJECT, buffer, len, true, charset);
        case JSONB_TYPE_SMALL_ARRAY:
            return parse_array_or_object(Json_enum_type.ARRAY, buffer, len, false, charset);
        case JSONB_TYPE_LARGE_ARRAY:
            return parse_array_or_object(Json_enum_type.ARRAY, buffer, len, true, charset);
        default:
            return parse_scalar(type, buffer, len, charset);
        }
    }

    private static Json_Value parse_array_or_object(Json_enum_type type, LogBuffer buffer, long len, boolean large,
                                                    Charset charset) {
        long offset_size = large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
        if (len < 2 * offset_size) {
            throw new IllegalArgumentException("illegal json data");
        }
        long element_count = read_offset_or_size(buffer, large);
        long bytes = read_offset_or_size(buffer, large);

        if (bytes > len) {
            throw new IllegalArgumentException("illegal json data");
        }
        long header_size = 2 * offset_size;
        if (type == Json_enum_type.OBJECT) {
            header_size += element_count * (large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL);
        }

        header_size += element_count * (large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL);
        if (header_size > bytes) {
            throw new IllegalArgumentException("illegal json data");
        }
        return new Json_Value(type, buffer.rewind(), element_count, bytes, large);
    }

    private static long read_offset_or_size(LogBuffer buffer, boolean large) {
        return large ? buffer.getUint32() : buffer.getUint16();
    }

    private static Json_Value parse_scalar(int type, LogBuffer buffer, long len, Charset charset) {
        switch (type) {
        case JSONB_TYPE_LITERAL:
            /* purecov: inspected */
            int data = buffer.getUint8();
            switch (data) {
            case JSONB_NULL_LITERAL:
                return new Json_Value(Json_enum_type.LITERAL_NULL);
            case JSONB_TRUE_LITERAL:
                return new Json_Value(Json_enum_type.LITERAL_TRUE);
            case JSONB_FALSE_LITERAL:
                return new Json_Value(Json_enum_type.LITERAL_FALSE);
            default:
                throw new IllegalArgumentException("illegal json data");
            }
        case JSONB_TYPE_INT16:
            return new Json_Value(Json_enum_type.INT, buffer.getInt16());
        case JSONB_TYPE_INT32:
            return new Json_Value(Json_enum_type.INT, buffer.getInt32());
        case JSONB_TYPE_INT64:
            return new Json_Value(Json_enum_type.INT, buffer.getLong64());
        case JSONB_TYPE_UINT16:
            return new Json_Value(Json_enum_type.UINT, buffer.getUint16());
        case JSONB_TYPE_UINT32:
            return new Json_Value(Json_enum_type.UINT, buffer.getUint32());
        case JSONB_TYPE_UINT64:
            return new Json_Value(Json_enum_type.UINT, buffer.getUlong64());
        case JSONB_TYPE_DOUBLE:
            return new Json_Value(Json_enum_type.DOUBLE, Double.valueOf(buffer.getDouble64()));
        case JSONB_TYPE_STRING:
            int max_bytes = (int) Math.min(len, 5);
            long tlen = 0;
            long str_len = 0;
            long n = 0;
            byte[] datas = buffer.getData(max_bytes);
            for (int i = 0; i < max_bytes; i++) {
                // get the next 7 bits of the length.
                tlen |= (datas[i] & 0x7f) << (7 * i);
                if ((datas[i] & 0x80) == 0) {
                    // The length shouldn't exceed 32 bits.
                    if (tlen > 4294967296L) {
                        throw new IllegalArgumentException("illegal json data");
                    }

                    // This was the last byte. Return successfully.
                    n = i + 1;
                    str_len = tlen;
                    break;
                }
            }

            if (len < n + str_len) {
                throw new IllegalArgumentException("illegal json data");
            }
            return new Json_Value(Json_enum_type.STRING, buffer.rewind()
                .forward((int) n)
                .getFixString((int) str_len, charset));
        case JSONB_TYPE_OPAQUE:
            /*
             * There should always be at least one byte, which tells the
             * field type of the opaque value.
             */
            // The type is encoded as a uint8 that maps to an
            // enum_field_types.
            int type_byte = buffer.getUint8();
            int position = buffer.position();
            // Then there's the length of the value.
            int q_max_bytes = (int) Math.min(len - 1, 5);
            long q_tlen = 0;
            long q_str_len = 0;
            long q_n = 0;
            byte[] q_datas = buffer.getData(q_max_bytes);
            for (int i = 0; i < q_max_bytes; i++) {
                // get the next 7 bits of the length.
                q_tlen |= (q_datas[i] & 0x7f) << (7 * i);
                if ((q_datas[i] & 0x80) == 0) {
                    // The length shouldn't exceed 32 bits.
                    if (q_tlen > 4294967296L) {
                        throw new IllegalArgumentException("illegal json data");
                    }

                    // This was the last byte. Return successfully.
                    q_n = i + 1;
                    q_str_len = q_tlen;
                    break;
                }
            }

            if (q_str_len == 0 || len < q_n + q_str_len) {
                throw new IllegalArgumentException("illegal json data");
            }
            return new Json_Value(type_byte, buffer.position(position).forward((int) q_n), q_str_len);
        default:
            throw new IllegalArgumentException("illegal json data");
        }
    }

    public static class Json_Value {

        Json_enum_type m_type;
        int m_field_type;
        LogBuffer m_data;
        long m_element_count;
        long m_length;
        String m_string_value;
        Number m_int_value;
        double m_double_value;
        boolean m_large;

        public Json_Value(Json_enum_type t) {
            this.m_type = t;
        }

        public Json_Value(Json_enum_type t, Number val) {
            this.m_type = t;
            if (t == Json_enum_type.DOUBLE) {
                this.m_double_value = val.doubleValue();
            } else {
                this.m_int_value = val;
            }
        }

        public Json_Value(Json_enum_type t, String value) {
            this.m_type = t;
            this.m_string_value = value;
        }

        public Json_Value(int field_type, LogBuffer data, long bytes) {
            this.m_type = Json_enum_type.OPAQUE; // 不确定类型
            this.m_field_type = field_type;
            this.m_data = data;
            this.m_length = bytes;
        }

        public Json_Value(Json_enum_type t, LogBuffer data, long element_count, long bytes, boolean large) {
            this.m_type = t;
            this.m_data = data;
            this.m_element_count = element_count;
            this.m_length = bytes;
            this.m_large = large;
        }

        public String key(int i, Charset charset) {
            m_data.rewind();
            int offset_size = m_large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
            int key_entry_size = m_large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
            int entry_offset = 2 * offset_size + key_entry_size * i;
            // The offset of the key is the first part of the key
            // entry.
            m_data.forward(entry_offset);
            long key_offset = read_offset_or_size(m_data, m_large);
            // The length of the key is the second part of the
            // entry, always two
            // bytes.
            long key_length = m_data.getUint16();
            return m_data.rewind().forward((int) key_offset).getFixString((int) key_length, charset);
        }

        public Json_Value element(int i, Charset charset) {
            m_data.rewind();
            int offset_size = m_large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
            int key_entry_size = m_large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
            int value_entry_size = m_large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
            int first_entry_offset = 2 * offset_size;
            if (m_type == Json_enum_type.OBJECT) {
                first_entry_offset += m_element_count * key_entry_size;
            }
            int entry_offset = first_entry_offset + value_entry_size * i;
            int type = m_data.forward(entry_offset).getUint8();
            if (type == JSONB_TYPE_INT16 || type == JSONB_TYPE_UINT16 || type == JSONB_TYPE_LITERAL
                || (m_large && (type == JSONB_TYPE_INT32 || type == JSONB_TYPE_UINT32))) {
                return parse_scalar(type, m_data, value_entry_size - 1, charset);
            }
            int value_offset = (int) read_offset_or_size(m_data, m_large);
            return parse_value(type, m_data.rewind().forward(value_offset), (int) m_length - value_offset, charset);
        }

        public StringBuilder toJsonString(StringBuilder buf, Charset charset) {
            switch (m_type) {
            case OBJECT:
                buf.append("{");
                for (int i = 0; i < m_element_count; ++i) {
                    if (i > 0) {
                        buf.append(", ");
                    }
                    buf.append('"').append(key(i, charset)).append('"');
                    buf.append(": ");
                    element(i, charset).toJsonString(buf, charset);
                }
                buf.append("}");
                break;
            case ARRAY:
                buf.append("[");
                for (int i = 0; i < m_element_count; ++i) {
                    if (i > 0) {
                        buf.append(", ");
                    }
                    element(i, charset).toJsonString(buf, charset);
                }
                buf.append("]");
                break;
            case DOUBLE:
                buf.append(Double.valueOf(m_double_value).toString().toLowerCase());
                break;
            case INT:
                buf.append(m_int_value.toString().toLowerCase());
                break;
            case UINT:
                buf.append(m_int_value.toString().toLowerCase());
                break;
            case LITERAL_FALSE:
                buf.append("false");
                break;
            case LITERAL_TRUE:
                buf.append("true");
                break;
            case LITERAL_NULL:
                buf.append("null");
                break;
            case OPAQUE:
                String text = null;
                if (m_field_type == MySQLStandardFieldType.MYSQL_TYPE_NEWDECIMAL.getId()) {
                    int precision = m_data.getInt8();
                    int scale = m_data.getInt8();
                    text = m_data.getDecimal(precision, scale).toPlainString();
                    buf.append(text);
                } else if (m_field_type == MySQLStandardFieldType.MYSQL_TYPE_TIME.getId()) {
                    long packed_value = m_data.getLong64();
                    if (packed_value == 0) {
                        text = "00:00:00";
                    } else {
                        long ultime = Math.abs(packed_value);
                        long intpart = ultime >> 24;
                        int frac = (int) (ultime % (1L << 24));
                        // text = String.format("%s%02d:%02d:%02d",
                        // packed_value >= 0 ? "" : "-",
                        // (int) ((intpart >> 12) % (1 << 10)),
                        // (int) ((intpart >> 6) % (1 << 6)),
                        // (int) (intpart % (1 << 6)));
                        // text = text + "." + usecondsToStr(frac, 6);
                        StringBuilder builder = new StringBuilder(17);
                        if (packed_value < 0) {
                            builder.append('-');
                        }

                        int d = (int) ((intpart >> 12) % (1 << 10));
                        if (d > 100) {
                            builder.append(String.valueOf(d));
                        } else {
                            appendNumber2(builder, d);
                        }
                        builder.append(':');
                        appendNumber2(builder, (int) ((intpart >> 6) % (1 << 6)));
                        builder.append(':');
                        appendNumber2(builder, (int) (intpart % (1 << 6)));

                        builder.append('.').append(usecondsToStr(frac, 6));
                        text = builder.toString();
                    }
                    buf.append('"').append(text).append('"');
                } else if (m_field_type == MySQLStandardFieldType.MYSQL_TYPE_DATE.getId()
                    || m_field_type == MySQLStandardFieldType.MYSQL_TYPE_DATETIME.getId()
                    || m_field_type == MySQLStandardFieldType.MYSQL_TYPE_TIMESTAMP.getId()) {
                    long packed_value = m_data.getLong64();
                    if (packed_value == 0) {
                        text = "0000-00-00 00:00:00";
                    } else {
                        // 构造TimeStamp只处理到秒
                        long ultime = Math.abs(packed_value);
                        long intpart = ultime >> 24;
                        int frac = (int) (ultime % (1L << 24));
                        long ymd = intpart >> 17;
                        long ym = ymd >> 5;
                        long hms = intpart % (1 << 17);
                        // text =
                        // String.format("%04d-%02d-%02d %02d:%02d:%02d",
                        // (int) (ym / 13),
                        // (int) (ym % 13),
                        // (int) (ymd % (1 << 5)),
                        // (int) (hms >> 12),
                        // (int) ((hms >> 6) % (1 << 6)),
                        // (int) (hms % (1 << 6)));
                        StringBuilder builder = new StringBuilder(26);
                        appendNumber4(builder, (int) (ym / 13));
                        builder.append('-');
                        appendNumber2(builder, (int) (ym % 13));
                        builder.append('-');
                        appendNumber2(builder, (int) (ymd % (1 << 5)));
                        builder.append(' ');
                        appendNumber2(builder, (int) (hms >> 12));
                        builder.append(':');
                        appendNumber2(builder, (int) ((hms >> 6) % (1 << 6)));
                        builder.append(':');
                        appendNumber2(builder, (int) (hms % (1 << 6)));
                        builder.append('.').append(usecondsToStr(frac, 6));
                        text = builder.toString();
                    }
                    buf.append('"').append(text).append('"');
                } else {
                    text = m_data.getFixString((int) m_length, charset);
                    buf.append('"').append(escapse(text)).append('"');
                }

                break;
            case STRING:
                buf.append('"').append(escapse(m_string_value)).append('"');
                break;
            case ERROR:
                throw new IllegalArgumentException("illegal json data");
            }

            return buf;
        }
    }

    private static StringBuilder escapse(String data) {
        StringBuilder sb = new StringBuilder(data.length());
        int endIndex = data.length();
        for (int i = 0; i < endIndex; ++i) {
            char c = data.charAt(i);
            if (c == '"') {
                sb.append("\\\"");
            } else if (c == '\n') {
                sb.append("\\n");
            } else if (c == '\r') {
                sb.append("\\r");
            } else if (c == '\\') {
                sb.append("\\\\");
            } else if (c == '\t') {
                sb.append("\\t");
            } else if (c < 16) {
                sb.append("\\u000");
                sb.append(Integer.toHexString(c));
            } else if (c < 32) {
                sb.append("\\u00");
                sb.append(Integer.toHexString(c));
            } else if (c >= 0x7f && c <= 0xA0) {
                sb.append("\\u00");
                sb.append(Integer.toHexString(c));
            } else {
                sb.append(c);
            }
        }
        return sb;
    }

    public static enum Json_enum_type {
        OBJECT, ARRAY, STRING, INT, UINT, DOUBLE, LITERAL_NULL, LITERAL_TRUE, LITERAL_FALSE, OPAQUE, ERROR
    }

    private static void appendNumber2(StringBuilder builder, int d) {
        if (d >= 10) {
            builder.append(digits[(d / 10) % 10]).append(digits[d % 10]);
        } else {
            builder.append('0').append(digits[d]);
        }
    }

    private static void appendNumber3(StringBuilder builder, int d) {
        if (d >= 100) {
            builder.append(digits[d / 100]).append(digits[(d / 10) % 10]).append(digits[d % 10]);
        } else {
            builder.append('0');
            appendNumber2(builder, d);
        }
    }

    private static void appendNumber4(StringBuilder builder, int d) {
        if (d >= 1000) {
            builder.append(digits[d / 1000])
                .append(digits[(d / 100) % 10])
                .append(digits[(d / 10) % 10])
                .append(digits[d % 10]);
        } else {
            builder.append('0');
            appendNumber3(builder, d);
        }
    }

    private static String usecondsToStr(int frac, int meta) {
        String sec = String.valueOf(frac);
        if (meta > 6) {
            throw new IllegalArgumentException("unknow useconds meta : " + meta);
        }

        if (sec.length() < 6) {
            StringBuilder result = new StringBuilder(6);
            int len = 6 - sec.length();
            for (; len > 0; len--) {
                result.append('0');
            }
            result.append(sec);
            sec = result.toString();
        }

        return sec.substring(0, meta);
    }
}
